package engine.systems.motion.orbit
{
	import engine.classes.enumerations.Quadrant;
	import engine.classes.enumerations.SystemPriority;
	import engine.groups.Group;
	import engine.systems.spatial.spatialOffset.SpatialOffsetData;
	import engine.systems.update.GameSystem;
	import engine.utils.MathUtils;
	
	import flash.display.DisplayObjectContainer;
	
	/**
	 * The Orbit System moves an Entity on an elliptical path around a given radius. An Orbit has a front
	 * and back Display Object Container name the system looks for to move Entities to the front and back of
	 * a common parent's display list to simulate orbital movement.
	 * 
	 * <p>If you have multiple Entities sharing the same front and back Display Object Containers to orbit,
	 * the system assumes they're all moving at the same rate. Finding Display Objects that are also orbiting
	 * AND calculating their display list order based on different orbiting rates seems very intensive, so
	 * it's not being done. 
	 * 
	 * @see Orbit
	 * @see OrbitNode
	 */
	public class OrbitSystem extends GameSystem
	{
		private const ORBIT:String = "orbit";
		
		private var cosine:Number;
		private var sine:Number;
		private var radius:Number;
		private var offset:Number;
		private var data:SpatialOffsetData;
		private var perpendicular:Number;
		private var parent:DisplayObjectContainer;
		
		public function OrbitSystem()
		{
			super(OrbitNode, updateNode, nodeAdded, nodeRemoved);
			
			this.priority = SystemPriority.PRE_MOTION;
		}
		
		private function updateNode(node:OrbitNode, time:Number):void
		{
			//Increment the Orbit's radians by a time rate.
			node.orbit.radians += node.orbit.rate * time;
			
			//Cosine and sine are used multiple times. Should only calculate this once.
			cosine 	= Math.cos(node.orbit.radians);
			sine 	= Math.sin(node.orbit.radians);
			
			//The radius given the current position in orbit.
			radius = sine * node.orbit.radius;
			
			/**
			 * An offset determines if the Entity's path is going to orbit away from its center radius
			 * to give it a more 3D look as it orbits.
			 */
			offset = cosine * node.orbit.offset;
			
			data = node.offset.get(ORBIT);
			
			//If the Orbit has a rotated/tilted axis, position it on its radius based on the tilt.
			data.x = Math.cos(node.orbit.rotation) * radius;
			data.y = Math.sin(node.orbit.rotation) * radius;
			
			/**
			 * The offset needs to be mutiplied by the sine and cosine of the Orbit's rotation. Math.PI * 0.5
			 * is added because the offset is applied perpendicular to the current rotation to position it
			 * correctly.
			 */
			perpendicular = node.orbit.rotation - Math.PI * 0.5;
			
			data.x += Math.cos(perpendicular) * offset;
			data.y += Math.sin(perpendicular) * offset;
			
			this.updateIndex(node);
		}
		
		/**
		 * Entities are orbiting in a circle separated by upper-left, upper-right, lower-right, and lower-left
		 * quadrants. Once an Entity has reached the edge of a quadrant (upper-most, lower-most, left-most,
		 * or right-most part of the circle), changes to the Entity's display list order need to be made.
		 */
		private function updateIndex(node:OrbitNode):void
		{
			parent = node.render.display.parent;
			
			switch(node.orbit.quadrant)
			{
				case Quadrant.UPPER_LEFT:
					if(node.orbit.rate > 0 && sine > 0)
					{
						node.orbit.quadrant = Quadrant.UPPER_RIGHT;
						parent.setChildIndex(node.render.display, 0);
					}
					else if(node.orbit.rate < 0 && cosine < 0)
					{
						node.orbit.quadrant = Quadrant.LOWER_LEFT;
						
						parent = parent.parent.getChildByName(node.orbit.childFront) as DisplayObjectContainer;
						parent.addChildAt(node.render.display, 0);
					}
					break;
				
				case Quadrant.UPPER_RIGHT:
					if(node.orbit.rate > 0 && cosine < 0)
					{
						node.orbit.quadrant = Quadrant.LOWER_RIGHT;
						
						parent = parent.parent.getChildByName(node.orbit.childFront) as DisplayObjectContainer;
						parent.addChildAt(node.render.display, 0);
					}
					else if(node.orbit.rate < 0 && sine < 0)
					{
						node.orbit.quadrant = Quadrant.UPPER_LEFT;
						
						parent.setChildIndex(node.render.display, 0);
					}
					break;
				
				case Quadrant.LOWER_RIGHT:
					if(node.orbit.rate > 0 && sine < 0)
					{
						node.orbit.quadrant = Quadrant.LOWER_LEFT;
						
						parent.setChildIndex(node.render.display, parent.numChildren - 1);
					}
					else if(node.orbit.rate < 0 && cosine > 0)
					{
						node.orbit.quadrant = Quadrant.UPPER_RIGHT;
						
						parent = parent.parent.getChildByName(node.orbit.childBack) as DisplayObjectContainer;
						parent.addChildAt(node.render.display, parent.numChildren - 1);
					}
					break;
				
				case Quadrant.LOWER_LEFT:
					if(node.orbit.rate > 0 && cosine > 0)
					{
						node.orbit.quadrant = Quadrant.UPPER_LEFT;
						
						parent = parent.parent.getChildByName(node.orbit.childBack) as DisplayObjectContainer;
						parent.addChildAt(node.render.display, parent.numChildren - 1);
					}
					else if(node.orbit.rate < 0 && sine > 0)
					{
						node.orbit.quadrant = Quadrant.LOWER_RIGHT;
						
						parent.setChildIndex(node.render.display, parent.numChildren - 1);
					}
					break;
			}
		}
		
		/**
		 * When a new OrbitNode is added, it needs to be positioned in the correct quadrant based in its
		 * initial radians value.
		 */
		private function setIndex(node:OrbitNode):void
		{
			var parent:DisplayObjectContainer = node.render.display.parent;
			var degrees:Number = MathUtils.toDegrees(node.orbit.radians);
			
			if(degrees >= -180 && degrees <= -90)
			{
				node.orbit.quadrant = Quadrant.LOWER_LEFT;
				parent = parent.parent.getChildByName(node.orbit.childFront) as DisplayObjectContainer;
				parent.addChildAt(node.render.display, 0);
			}
			else if(degrees > -90 && degrees <= 0)
			{
				node.orbit.quadrant = Quadrant.UPPER_LEFT;
				parent = parent.parent.getChildByName(node.orbit.childBack) as DisplayObjectContainer;
				parent.addChildAt(node.render.display, parent.numChildren - 1);
			}
			else if(degrees > 0 && degrees <= 90)
			{
				node.orbit.quadrant = Quadrant.UPPER_RIGHT;
				parent = parent.parent.getChildByName(node.orbit.childBack) as DisplayObjectContainer;
				parent.addChildAt(node.render.display, 0);
			}
			else if(degrees > 90 && degrees <= 180)
			{
				node.orbit.quadrant = Quadrant.LOWER_RIGHT;
				parent = parent.parent.getChildByName(node.orbit.childFront) as DisplayObjectContainer;
				parent.addChildAt(node.render.display, parent.numChildren - 1);
			}
		}
		
		private function nodeAdded(node:OrbitNode):void
		{
			node.offset.add(ORBIT, new SpatialOffsetData());
			
			this.setIndex(node);
		}
		
		private function nodeRemoved(node:OrbitNode):void
		{
			node.offset.remove(ORBIT);
		}
	}
}